﻿#include "anip.h"
#include "transform.h"
#include "font.h"
#include "target.h"
#include <vector>
#include <map>
#include <fstream>
#include <sstream>

#include <locale>

using namespace std;
using namespace anip;

map<string, float> configs
{
	{"speed", 1},
	{"repulsion", 50},
	{"fieldP", 2},
	{"damping", 0.2},
	{"fadeP", 1},
	{"fadeF", 1},
	{"widthP", 2},
	{"widthF", 1},
	{"inputP", 1},
	{"inputF", 1},
	{"inputS", 0},
	{"allowNegWeight", 0}
};

struct Point
{
	vec2 pos, prevPos, accel;
	wstring label;
};

struct edge
{
	wstring a, b;
	float w;
};

vector<edge> edges;
vector<Point> points;
vector<vector<float>> forceMatrix;
map<wstring, int> nameToPoint;
Transform2D view;

Font font = Font::null();

int selectedPoint = -1;

float inputWeightToForce(float x)
{
	x = x + configs["inputS"];
	if (configs["allowNegWeight"] == 0)
		x = max(0.0f, x);
	return powf(x, configs["inputP"]) * configs["inputF"];
}

void readInput(string filename)
{
	cout << "File:" << endl << readFile(filename) << endl;
	wstring file = readUTF8File(filename);
	cout << "Cout:" << endl << ws2utf8s(file) << endl;
	cout << "Wcout:" << endl;
	wcout << file << endl;
	
	auto lines = split(file, L"\n");

	// process nodes
	{
		auto nodes = split(lines[0], L",");
		int n = nodes.size();
		for (int i = 0; i < n; i++)
		{
			float angle = (float)i / n * 2 * 3.1416f;
			vec2 pos = n * 0.5f * vec2{ sin(angle), cos(angle) };
			points.push_back(Point{ pos, pos, vec2{}, nodes[i] });
			nameToPoint.insert({ nodes[i], i });

			forceMatrix.emplace_back(n, 0.0f);
		}
		cout << "Read " << n << " nodes ";
	}

	// read edges
	for (auto& line : lines)
	{
		auto abw = split(line, L"=");
		if (abw.size() < 2) continue;
		auto ab = split(abw[0], L"<->");
		if (ab.size() < 2) continue;

		float w = inputWeightToForce(stof(abw[1]));
		if (w == 0.0f) continue;

		int ia = nameToPoint[ab[0]];
		int ib = nameToPoint[ab[1]];
		edges.push_back({ ab[0], ab[1], w });

		forceMatrix[ia][ib] = w;
		forceMatrix[ib][ia] = w;
	}
	cout << " and " << lines.size() - 1 << " edges" << endl;
}

void renderFrame()
{
	const color background = Colors::white;
	const color pointLineColor = { 0.2f, 0.2f, 0.6f, 1.0f };
	const color springColor = { 0.13f, 0.4f, 0.4f, 1.0f };
	const vec2 pointSize = { 2.0f, 2.0f };

	Context::noScissor();
	Context::clear(Colors::white);
	
	Context::currentTarget().transformReset();
	Context::currentTarget().transformPushCombined(view);

	for (auto& e : edges)
	{
		int a = nameToPoint[e.a];
		int b = nameToPoint[e.b];
		color col = springColor;
		if (selectedPoint != a && selectedPoint != b)
			col.a = powf(e.w, configs["fadeP"]) * configs["fadeF"];
		float wid = powf(e.w, configs["widthP"]) * configs["widthF"];
		Draw::line(points[a].pos, points[b].pos, wid, col);
	}
	
	for (auto& pt : points)
	{
		Draw::ellipseOutline(pt.pos - pointSize * 0.5f, pointSize, 0.1f, pointLineColor);
		Draw::text(pt.pos, pt.label, Colors::black, font, { 0.5f, 0.5f });
	}
}

void applyForces()
{
	for (unsigned int i = 0; i < points.size(); i++)
	{
		vec2 pos = points[i].pos;
		vec2 accel{ 0 };
		for (unsigned int j = 0; j < points.size(); j++)
		{
			if (i == j) continue;
			vec2 pos2 = points[j].pos;
			float dist = math::distance(pos, pos2);
			accel += (pos2 - pos) / dist * (
				- configs["repulsion"] / powf(dist, configs["fieldP"])	// repulsion
				+ forceMatrix[i][j] * dist);		// attraction
		}
		points[i].accel = accel;
	}
}

bool integrateMotion()
{
	float timestep = configs["speed"] * (float)Context::dampedDeltaTime();
	bool change = false;
	for (auto& pt : points)
	{
		vec2 oldPos = pt.pos;
		vec2 velocity = (pt.pos - pt.prevPos) 
			* powf(configs["damping"], timestep);
		if (math::length(velocity) > 0.01f) change = true;

		pt.pos += velocity + pt.accel * timestep * timestep;
		pt.prevPos = oldPos;
	}
	return change;
}

void handleEvents()
{
	static enum {
		None,
		PointDrag,
		Pan
	} mode = None;
	static vec2 mpos{}, dragStart{}, oldOffset{};
	
	UserEvent ev;
	while (Context::pollEvent(ev))
	{
		switch (ev.type)
		{
		case UserEvent::Type::MouseAction:
			if (ev.mouse.action == ButtonAction::Down)
			{
				if (ev.mouse.button == MouseButton::Left)
				{
					mode = PointDrag;
					selectedPoint = -1;
					for (unsigned int i = 0; i < points.size(); i++)
						if (math::distance(points[i].pos, mpos) < 1.0f)
						{
							selectedPoint = i;
							break;
						}
					if (selectedPoint == -1)
						mode = None;
				}
				else if (ev.mouse.button == MouseButton::Right)
				{
					mode = Pan;
					oldOffset = view.offset;
					dragStart = mpos;
				}
			}
			else if (ev.mouse.action == ButtonAction::Up)
			{
				mode = None;
			}
			break;
		case UserEvent::Type::MouseMove:
			mpos = view.undoTransform(ev.pos);
			break;
		case UserEvent::Type::Scroll:
			view = view.scaleAround(mpos, vec2{ powf(1.05f, ev.scroll.y) });
			break;
		}
	}

	switch (mode)
	{
	case PointDrag:
		points[selectedPoint].pos = mpos;
		break;
	case Pan:
		mpos = view.undoTransform(Context::mousePos());
		view.offset += (mpos - dragStart) * view.zoom;
		break;
	}
}

void help()
{
	cout << "netvis -- Visualize a network graph as a spring system"	<< endl;
	cout																<< endl;
	cout << "Usage:"													<< endl;
	cout << "   netvis [filename] [[-option] [value] ...]"				<< endl;
	cout																<< endl;
	cout << "Input Format:"												<< endl;
	cout << "   The first line consists of the names of all the nodes"	<< endl;
	cout << "in the network, separated by commas (`,`); whitespaces"	<< endl;
	cout << "are not trimmed. Each line after this specifies an edge"	<< endl;
	cout << "in the format of `A<->B=W` (again, no trimming of white-"	<< endl;
	cout << "spaces), A and B being names of the nodes that the edge"	<< endl;
	cout << "connects and W being the weight of the edge."				<< endl;
	cout																<< endl;
	cout << "Options:"													<< endl;
	cout << "    -speed      (default 1) The speed of the simulation."	<< endl;
	cout << "    -damping    (default 0.2) Damping factor applied to"	<< endl;
	cout << "                the nodes. Greater = freer motion."		<< endl;
	cout << "    -repulsion  (default 50)"								<< endl;
	cout << "    -fieldP     (default 2)"								<< endl;
	cout << "                The force of repulsion between nodes are"	<< endl;
	cout << "                calculated as `F = R / distance ^ P`."		<< endl;
	cout << "    -fadeF      (default 1)"								<< endl;
	cout << "    -fadeP      (default 1)"								<< endl;
	cout << "    -widthF     (default 2)"								<< endl;
	cout << "    -widthP     (default 1)"								<< endl;
	cout << "                The transparency (0-1) and width of lines" << endl;
	cout << "                representing edges are calculated using"	<< endl;
	cout << "                `V = weight ^ P * F`."						<< endl;
	cout << "    -inputP     (default 1)"								<< endl;
	cout << "    -inputF     (default 1)"								<< endl;
	cout << "    -inputS     (default 0)"								<< endl;
	cout << "    -allowNegWeight (default 0)"							<< endl;
	cout << "                Preprocessing of edge weights is done by"	<< endl;
	cout << "                `W_new = (weight + S) ^ P * F`. If `allow" << endl;
	cout << "                NegWeight` is set to 0, `weight + S` is"	<< endl;
	cout << "                clamped to be nonnegative before further"	<< endl;
	cout << "                calculation." << endl;
	exit(0);
}

int main(int argc, char** argv)
{
	if (argc == 1)
		help();
	else
	{
		string filename = argv[1];
		int i = 2;
		while (i < argc)
		{
			string arg = argv[i++];
			if (arg.size() < 2 || arg[0] != '-') help();
			configs[arg.substr(1)] = stof(argv[i++]);
		}
		readInput(filename);
	}

	anip::init();
	Context::init({ 800, 600 });
	Context::maxFramerate = 50;
	FontManager::addFace("std", "C:\\Windows\\Fonts\\simhei.ttf");
	FontManager::init();

	font = FontManager::get("std", 16);

	view.offset = Context::size() * 0.5f;
	view.zoom = vec2{ 10.0f };

	Context::show();
	while (!Context::windowShouldClose())
	{
		Context::startFrame();
		handleEvents();
		renderFrame();
		applyForces();
		integrateMotion();
		Context::update();
		Context::endFrame();
		Context::doEvents();
	}
	
	FontManager::finalize();
	anip::finalize();

	return 0;
}